Repository navigation
test: verify real Whisper inference on NVIDIA T4 - #9
Merged
Merged
Conversation
…level The only real-model test decoded silence on CPU and checked that the result was a string. Replace it with eight tests (marker `whisper`, still deselected by default) that separate adapter, server and WebSocket claims: - adapter: the device and compute type CTranslate2 reports in effect (exactly float16 on CUDA), silence and empty audio, real reference speech with a corpus-WER regression guard, deterministic final decoding, and decode failures surfacing as InferenceError - server: a real uvicorn server loading the model from Settings and reporting it on /health; shutdown stops the inference workers and frees the port - WebSocket: a real client streams reference speech; partials, exactly one final, close 1000, and the final equals a direct adapter decode The speech tests require the recorded Phase 1 audio set (checked by PCM SHA-256) so WER is comparable to the recorded T4 FP16 values. CUDA requested without a GPU fails instead of skipping. benchmarks/whisper_verification.py runs the suite and records UTC time, commit, nvidia-smi, versions, the exact command, any pytest overrides, and per-test outcomes. docs/whisper-verification.md documents the suite and the one-cell Colab procedure; README and COLAB.md point to it. GPU verification is pending: no real-model run has been made yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DfLG9q5h7eiawGNwaBswq
test_runner_counts_timeouts_while_queued slept a fixed 0.4 s and then asserted the timed-out decode had released its slot. The decode runs 0.5 s in a worker thread and the release happens on the loop afterwards, so the assertion had ~100 ms of slack and failed on a loaded macOS runner (CI run 40155745468, attempt 1: in_flight == 1). Not a cleanup bug: the slot is always released shortly after and returned to the pool. The test now uses GatedTranscriber, so the test decides when the decode finishes, waits on state with a bounded until(), asserts the slot is held while the decode runs and returned (free == 1) afterwards, and always shuts the runner down. whisper_verification: complete now requires exactly the 8 expected tests passing with no failures, errors or skips, and the runtime identity recorded during the run (model and CTranslate2 classes, device, compute type, CUDA device count, base.en snapshot path and model.bin hash) to match the request, a T4 in nvidia-smi for CUDA, no PYTEST_ADDOPTS or PYTHONPATH override, and a clean checkout. Requested settings are not treated as evidence. Validator rules are tested offline. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DfLG9q5h7eiawGNwaBswq
The real-model suite ran on a Colab Tesla T4 at 096a034: 8 of 8 passed with faster-whisper base.en on CUDA FP16, and the recorder reported complete: true with no problems. Corpus WER 0.0531, streamed-utterance WER 0.1154 (v2 normalizer); bounds unchanged. - docs/benchmarks/whisper-t4/raw: record.json, pytest.log and junit.xml, committed byte-identical to the returned ZIP, with SHA256SUMS (checked by verify_evidence: now 4 manifests, 173 files). A README lists the independent checks made on them: JUnit re-parsed to the record, the validator re-run on raw inputs, every JUnit test line matched to the source at the recorded commit, and all five transcripts identical to Phase 1's FP16 hypotheses. - docs/whisper-verification.md: verified status, filled log, limitations, and the Colab procedure including the cuBLAS 12 / cuDNN 9 library-path fix and running the recorder without Colab's default PYTHONPATH. The failed attempts are documented as a troubleshooting incident from the operator's account; their output is not committed. Corrects the cited baseline: Phase 1's 0.0708 was scored with the v1 normalizer; the comparable v2 value is 0.0531. - README and COLAB.md: remove the GPU-pending statements, update test counts (269 offline per configuration) and the evidence inventory, and clone main instead of the stale feat/streaming-mvp branch. - .gitignore: allow the committed evidence pytest.log. The tests and the recorder are unchanged since 096a034, so the code on this branch is the code that was verified. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DfLG9q5h7eiawGNwaBswq
test_runner_releases_slot_only_when_thread_finishes slept a fixed 0.4 s after a timed-out 0.3 s decode and then asserted the slot was released. The release runs on the loop after the worker thread returns, so the assertion had ~200 ms of slack: the same defect class as the macOS flake fixed in 096a034. With the worker thread returning 0.25 s late (a loaded runner), it fails with in_flight == 1. It now waits on state with gated.until(), keeps every assertion, and always shuts the runner down. The macOS job failed once more on ee4b7f3 (docs-only change; passed on rerun). Its log is not readable from this environment, so this test is the likeliest match for that failure, not a confirmed one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DfLG9q5h7eiawGNwaBswq
hydrangeas20
added a commit
that referenced
this pull request
Oct 25, 2026
* test: replace fixed sleeps and unbounded polls in lifecycle/reliability tests Audit of timing-sensitive tests (lifecycle, reliability, live server, streaming, errors, load-test, observability, priority). Changed only where fragility was demonstrated; behavioral assertions are unchanged. - test_lifecycle mid-partial cancellation: a fixed 0.4 s sleep after a 0.3 s decode asserted the slot was released (~100 ms slack, the macOS flake class fixed in PR #9). With the worker thread returning 0.25 s late the old test fails (assert 1 == 0); it now waits on state with gated.until() and always shuts the runner down. - test_reliability slow sender: frames every 0.35 s against a 0.5 s idle limit left 150 ms of margin; one late client wake-up fires idle_timeout. Now 0.1 s of audio every 0.15 s: still slower than real time (1.5x), 0.35 s margin. Passes with a 0.2 s late wake-up injected; still fails if the idle timer stops resetting per frame (whole stream > 0.5 s). - Five unbounded `while ...: sleep` polls (lifecycle, and the reliability final-decode disconnect, queued disconnect, malformed-message and shutdown tests) now use bounded condition waits: a regression that prevents the awaited state fails in ~5 s with a message instead of hanging until the CI job timeout (demonstrated by disabling partial scheduling: old versions hang, new fail with "timed out waiting for partial decode to start"). Left as is (already bounded, deliberate, or wide margins): live-server polls, idle/send/load-test wall-clock bounds (2-3 s for 0.3-0.5 s operations), the event-loop-lag test, gate pumps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DfLG9q5h7eiawGNwaBswq * feat(verification): record environment provenance; correct WER comment Recorder (benchmarks/whisper_verification.py): a `provenance` section that never changes `complete`, separating what the environment said from what the test process loaded: - environment_metadata: allowlisted CUDA / loader / pytest variables as passed to pytest (LD_LIBRARY_PATH as entries, LD_PRELOAD, CUDA_*, NVIDIA_*, CT2_*, PYTEST_DISABLE_PLUGIN_AUTOLOAD), which search-path directories hold libcublas.so.12 / libcublasLt.so.12 / libcudnn.so.9, and installed nvidia-* wheels. Nothing outside the allowlist is read; URL userinfo and secret-looking KEY=value entries are redacted before path lists are split. Labelled as metadata, not proof of loading. - pytest_plugins: pytest11 entry points with distribution versions, and the plugins line pytest printed for the run. - loaded_libraries: CUDA / CTranslate2 shared objects mapped into the pytest process after the real-speech decodes (/proc/self/maps), as recorded by that test. PYTEST_PLUGINS is now treated as an override like PYTEST_ADDOPTS and PYTHONPATH (it loads arbitrary plugin modules): a stricter check, no existing requirement relaxed. tests/test_whisper.py: the real-speech test records the loaded CUDA libraries; the 0.0708 baseline comment is corrected (Phase 1 scored the same transcripts with the v1 normalizer; the v2 value the tests compute is 0.0531). WER bounds unchanged. Offline tests for every new recorder behaviour (allowlist, redaction, search-path detection, wheels, plugin inventory and header parsing, loaded-library parsing, provenance not affecting completeness). Docs: the T4 result stays tied to 096a034; the changed recorder and test file are marked as not yet re-verified on a GPU. Provenance fields, their limits, and how to reproduce the verified environment are documented. PR #9's raw evidence is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DfLG9q5h7eiawGNwaBswq * fix(verification): close review findings F1-F6 before GPU verification F1 Unreadable library-path directories no longer crash the recorder: Path.exists/is_dir raised PermissionError under an unreadable parent (reproduced as a non-root user on 3.11 and 3.13), aborting record.json after the tests ran. Each probe now records the error (ld_library_path_entries_exist, search_errors). Each provenance section is collected independently; one that raises is recorded as collection_error and fails the run closed, instead of losing the record. F2 /proc/self/maps: the pathname is everything after the fifth field, so paths with spaces ("My Drive") are kept whole (was: last token only); " (deleted)" is dropped. The parser moved to the recorder module and is tested offline, including against a real mapping in this process. F3 Plugin inventory renamed and documented accurately: installed_entry_points (what pytest can auto-load) and header_reported (pytest's plugins: line, entry-point plugins only). Neither claims to list every plugin that ran: -p, PYTEST_PLUGINS, conftest.py and pytest_plugins routes are covered by the override and checkout rules. F4 Redaction: URL userinfo is removed up to the last "@" (passwords with "@" or "/" previously leaked, in part or whole); secret-like KEY=value assignments are redacted anywhere, including query strings; common token formats (GitHub, Hugging Face, AWS, sk-, Slack, GitLab, JWT) are redacted. Documented as best effort: the allowlist remains the main control and unknown formats are not caught. F5 Checkout audit (fail closed), taken before pytest starts: any untracked non-ignored file anywhere, or any ignored *.py/*.pyc/*.pyo/*.pth/*.so or pytest/Python config file directly in the repo root, tests/ or src/, makes the run incomplete; an audit failure does too. git status --untracked-files=no (the dirty check) let an untracked root conftest.py run inside the tests while reporting a clean checkout; reproduced, and now reported as "untracked files present (1): ['conftest.py']". F6 LD_PRELOAD is split on spaces and colons, mixed; the lifecycle test cancels its session task if a bounded wait fails. Existing requirements (exact 8 tests, model identity, WER bounds 0.12 / 0.25, clean commit, overrides) are unchanged. No production code, dependency, CI or raw-evidence change. Not yet re-verified on a GPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DfLG9q5h7eiawGNwaBswq --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The offline suite (fake and gated backends, run in CI) is extensive, but the only real-model test,
tests/test_whisper.py, loadedbase.enon CPU withint8, decoded silence, and checked that the result was a string. It did not check CUDA, FP16, transcript correctness, error handling, the server, or the WebSocket path, and it had not been confirmed as executed in the current verification cycle.Implementation
tests/test_whisper.py: 8 tests, markerwhisper, still deselected by default. Each level claims only what it exercises.…loads_with_requested_device_and_compute_typefloat16, so a silent fallback fails…decodes_silence_and_empty_audio""…transcribes_real_speech…final_decode_is_deterministic…wraps_decode_errorsInferenceErrortest_server_loads_real_model_from_settingsSettings;/healthreportswhisper/base.en/ device / compute typetest_websocket_streams_real_speechready, ≥1 non-empty partial with consecutiveseq, exactly onefinal, noerror, close 1000; final text equals a direct adapter decode of the same audio; WER ≤ 0.25test_server_shutdown_releases_inference_workersinfer*worker threads exit, and the port is releasedFixtures and thresholds:
python -m benchmarks.prepare_audio --num 5and check each file's PCM SHA-256 against the recorded Phase 1 precision run, so WER is computed on the audio whose FP16 WER was recorded.precision_t4.json) and 0.1154 for the streamed utterance (3/26,gpu_smoke.json).ECHORELAY_TEST_DEVICE=cudaselects the GPU. A CUDA request with no visible device fails; it does not skip.benchmarks/whisper_verification.py(new, standard library + existing helpers): runspython -m pytest -m whisper -rA -s tests/test_whisper.pyand writesrecord.json,pytest.logandjunit.xml. The record holds:nvidia-smioutput, GPU and driverPYTEST_ADDOPTS/PYTHONPATH/ECHORELAY_MODEL_DIRoverrides, so a stubbed run cannot pass for a real onecompleterequires, andproblemslists every violation of:faster_whisper.transcribe.WhisperModeloverctranslate2._ext.Whisperfloat16on CUDA)models--Systran--faster-whisper-base.ensnapshot faster-whisper resolved, with themodel.binSHA-256nvidia-smilists a T4 for CUDA (--expect-gpu)PYTEST_ADDOPTS/PYTHONPATHoverride, and a clean checkouttests/test_whisper_verification.py(32 offline tests, run in CI) covers each rule.tests/test_observability.py: macOS flake fix.test_runner_counts_timeouts_while_queued(CI run 40155745468, attempt 1:in_flightwas 1 afterawait asyncio.sleep(0.4)).free == 1. The scheduler correctly skips the timed-out waiter.GatedTranscriber, so the test decides when the decode finishes.until(), and then assertsin_flight == 0,waiting == 0,free == 1, and that the queued request never ran.runner.shutdown()is in afinally.Evidence and docs (
ee4b7f3).docs/benchmarks/whisper-t4/raw/:record.json,pytest.logandjunit.xml, committed byte-identical to the returned ZIP, withSHA256SUMS.verify_evidencenow checks 4 manifests and 173 files.junit.xml.docs/benchmarks/whisper-t4/README.md: the result, provenance, the independent checks below, and comparisons with earlier T4 evidence.docs/whisper-verification.md: the claims of each test, verified status, the filled verification log, limitations, and the reproducible two-cell Colab procedure.README.md:benchmarks/COLAB.md:maininstead of the oldfeat/streaming-mvpbranch.PYTHONPATH..gitignore: a narrow exception so the evidencepytest.logcan be committed, like the existing one forserver.log.Unchanged:
tests/test_whisper.py,benchmarks/whisper_verification.pyandsrc/are byte-identical to096a034, the verified commit.tests/test_units.py: second flake fix (0858899).ee4b7f3, a docs-only commit, the macOS job failed once more and passed on rerun.test_runner_releases_slot_only_when_thread_finisheshas the same defect class as the flake fixed in096a034. It sleeps a fixed 0.4 s after a timed-out 0.3 s decode, then asserts the slot was released, leaving about 200 ms of slack.assert 1 == 0onin_flight.until(), keeps every assertion, and always shuts the runner down.Real-model verification (T4)
096a0347b620e4e5a8696cd6288379c3a3ccae77, clean checkoutfaster_whisper.transcribe.WhisperModeloverctranslate2._ext.Whisper, devicecuda, compute typefloat16, 1 CUDA device, English-only,models--Systran--faster-whisper-base.ensnapshot3d3d5dee…complete: true,problems: []; noPYTEST_ADDOPTS/PYTHONPATHoverrideIndependent checks on the evidence. These were made on the files themselves, without trusting the recorder's verdict:
junit.xmlre-parsed reproducesrecord.json's results exactly.tests/test_whisper.pyat096a034, so the suite that ran is the committed one.pytest.logagrees, and its timestamps fall inside the run window.precision_t4.json). That run used the same GPU, driver and library versions.Correction. The suite cited Phase 1's 0.0708 as the recorded baseline, but that was scored with the v1 normalizer. The comparable v2 value is 0.0531, and the docs now say so. The comment in
tests/test_whisper.pystill says 0.0708, left as is so the verified file stays byte-identical. The bounds themselves are unchanged.Troubleshooting incident (operator's account; output not committed).
Library libcublas.so.12 is not found or cannot be loaded.libcublas.so.13, withLD_LIBRARY_PATH=/usr/lib64-nvidia.libcublas.so.12lived underdist-packages/nvidia/cublas/lib, outside that path.LD_LIBRARY_PATH, plus cuDNN 9's directory if present.NOT COMPLETEbecause of Colab's defaultPYTHONPATH=/env/python. The check was not relaxed.PYTHONPATHremoved from the recorder's environment, it exited 0 and reportedcomplete: true.LD_LIBRARY_PATHis not captured by the record, so the fix appears only in this account.Offline results (exact)
Local, Linux, no GPU, at
0858899. Apart fromtests/test_units.py, the code is identical to096a034; the rest of the change is docs and evidence.python -m pytest -q -m "not whisper", all 4 flag combinationspython -m pytest -m whisper --collect-only -qpython -m benchmarks.verify_evidencegit diff --checkEarlier, at
096a034:test_observability.pyandtest_whisper_verification.pypassed 30/30 repeats on both versions.ruff check --select F,E9was clean on new and changed files.The repo has no configured lint or type checker. 269 = 237 existing tests + 32 validator tests.
Pre-GPU harness checks (not model evidence): a stub
WhisperModelexercised the harness and recorder; the results are described indocs/whisper-verification.md.Reproducing
Use the two Colab cells in
docs/whisper-verification.md. They include the library-path fix and run the recorder withoutPYTHONPATH.Remaining limitations
base.en, CUDA FP16, one Colab T4, one run.int8,int8_float16, other model sizes, other GPUs, other driver or library versions.LD_LIBRARY_PATH, which was required on Colab.typeguard,anyio,langsmith,platformdirs). They appear only inpytest.log. The integrity check covers overrides, not installed entry-point plugins.model.binhash has nothing to compare against. It is recorded for traceability; no independent reference hash exists in the repo.test_runner_counts_timeouts_while_queued(096a034, confirmed from the log).test_runner_releases_slot_only_when_thread_finishes(0858899, the likeliest match, not confirmed).0858899passed 10/10 on the first attempt (run 40163113395).test_lifecycle.py,test_reliability.pyandtest_live_server.py.https://claude.ai/code/session_017DfLG9q5h7eiawGNwaBswq
Generated by Claude Code